package mmc.server.shell;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import mmc.server.shell.ops.ArgumentList;
import mmc.server.shell.ops.Operation;

public class Util {
	
	public static Map<Class<?>,Class<?>> primitiveToWrapper;
	public static Map<Class<?>,Class<?>> wrapperToPrimitive;
	public static Set<Class<?>> wrappers;
	public static Set<Class<?>> primitives;
	
	static{
		primitiveToWrapper = new HashMap<Class<?>,Class<?>>();
		wrapperToPrimitive = new HashMap<Class<?>,Class<?>>();
		wrappers = new HashSet<Class<?>>();
		primitives = new HashSet<Class<?>>();
		
		primitiveToWrapper.put(int.class, Integer.class);
		primitiveToWrapper.put(char.class, Character.class);
		primitiveToWrapper.put(short.class, Short.class);
		primitiveToWrapper.put(byte.class, Byte.class);
		primitiveToWrapper.put(long.class, Long.class);
		primitiveToWrapper.put(float.class, Float.class);
		primitiveToWrapper.put(double.class, Double.class);
		primitiveToWrapper.put(boolean.class, Boolean.class);
		
		for(Class<?> primitive : primitiveToWrapper.keySet()){
			Class<?> wrapper = primitiveToWrapper.get(primitive);
			wrapperToPrimitive.put(wrapper, primitive);
			wrappers.add(wrapper);
			primitives.add(primitive);
		}
	}
	
	public static boolean isWrapper(Class<?> clasz){
		return wrappers.contains(clasz);
	}
	
	public static boolean isPrimitive(Class<?> clasz){
		return primitives.contains(clasz);
	}
	
	public static boolean isNumeric(Class<?> clasz){
		return wrappers.contains(clasz)||primitives.contains(clasz);
	}
	
	private static void checkStaticFieldAccess(Field field){
		if(!Modifier.isStatic(field.getModifiers())){
			throw new RuntimeException("Cannot acces non-static field without reference: "+field);
		}
	}
	
	private static void checkWriteFieldAccess(Field field){
		if(Modifier.isFinal(field.getModifiers())){
			throw new RuntimeException("Cannot write to final field: "+field);
		}
	}
	
	private static void checkFieldAccess(Field field){
		/*
		if(!field.isAccessible()){
			throw new RuntimeException("Cannot access field: "+field);
		}*/
	}
	
	private static void checkStaticMethodAccess(Method method){
		if(!Modifier.isStatic(method.getModifiers())){
			throw new RuntimeException("Cannot acces non-static method without reference: "+method);
		}
	}
	
	private static void checkMethodAccess(Method Method){
		/*
		if(!field.isAccessible()){
			throw new RuntimeException("Cannot access field: "+field);
		}*/
	}
	
	public static Object getComplexField(Field field, Object o){
		checkFieldAccess(field);
		if(o==null)	checkStaticFieldAccess(field);
		
		try{
			return field.get(o);
		}catch(Exception e){
    		throw new RuntimeException(e.getMessage(),e);
    	}
	}
	
	public static void setComplexField(Field field, Object value, Object o){
		checkWriteFieldAccess(field);
		if(o==null)	checkStaticFieldAccess(field);
		
    	try{
	    	field.set(o, value);
    	}catch(Exception e){
    		throw new RuntimeException(e.getMessage(),e);
    	}
    }
	
	public static Object callComplexMethod(Method method, Object object, ArgumentList argumentList){
		checkMethodAccess(method);
		if(object==null) checkStaticMethodAccess(method);
		
		Class<?>[] pts = method.getParameterTypes();
		Vector<Operation> ops = argumentList.getArguments();
		Object[] args=new Object[pts.length];
		
		if(method.isVarArgs()){
			int i;
			
			for(i=0;i<args.length-1;i++)
				args[i] = convertFromDouble(ops.get(i).executeComplex(), pts[i]);
			
			Object[] varParam = new Object[ops.size()-i+1];
			
			for(int j=0;j+i<ops.size();j++){
				varParam[j] = convertFromDouble(ops.get(i+j).executeComplex(), Object.class);
			}
			
			args[args.length-1]=varParam;
		}else{
			for(int i=0;i<args.length;i++){
				args[i] = convertFromDouble(ops.get(i).executeComplex(), pts[i]);
			}
		}
		
		try{
			return method.invoke(object, args);
		}/*catch(InvocationTargetException e){
			if(e.getCause() instanceof RuntimeException)
				throw (RuntimeException)e.getCause();
			else
				throw new RuntimeException(e.getMessage(),e);
		}*/catch(Exception e){
    		throw new RuntimeException(e.getMessage(),e);
    	}
	}
	
	public static Object invokeConstructor(Constructor<?> con, Vector<Operation> ops){
		Object[] args=new Object[ops.size()];
		Class<?>[] pts = con.getParameterTypes();
		
		if(con.isVarArgs()){
			int i;
			
			for(i=0;i<args.length-1;i++)
				args[i] = convertFromDouble(ops.get(i).executeComplex(), pts[i]);
			
			Object[] varParam = new Object[ops.size()-i+1];
			
			for(int j=0;j+i<ops.size();j++){
				varParam[j] = convertFromDouble(ops.get(i+j).executeComplex(), Object.class);
			}
			
			args[args.length-1]=varParam;
		}else{
			for(int i=0;i<args.length;i++){
				args[i] = convertFromDouble(ops.get(i).executeComplex(), pts[i]);
			}
		}
		
		try{
			return con.newInstance(args);
		}catch(Exception e){
    		throw new RuntimeException(e.getMessage(),e);
    	}
	}
	
	public static double getField(Field field, Object o){
		checkFieldAccess(field);
		if(o==null)	checkStaticFieldAccess(field);
		
		try {
			return field.getBoolean(o)?1:0;
		} catch (Exception e) {}
		
		try {
			return field.getDouble(o);
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(),e);
		}
	}
	
	public static void setField(Field field, Object value, Object o){
		checkWriteFieldAccess(field);
		if(o==null)	checkStaticFieldAccess(field);
		
    	try{
			if(field.getType().equals(boolean.class))
	    		field.setBoolean(o, assignToDouble(value)!=0);
	    	else if(field.getType().equals(long.class))
	    		field.setLong(o, (long) assignToDouble(value));
	    	else if(field.getType().equals(char.class))
	    		field.setChar(o, (char) assignToDouble(value));
	    	else if(field.getType().equals(int.class))
	    		field.setInt(o, (int) assignToDouble(value));
	    	else if(field.getType().equals(short.class))
	    		field.setShort(o, (short) assignToDouble(value));
	    	else if(field.getType().equals(byte.class))
	    		field.setByte(o, (byte) assignToDouble(value));
	    	else if(field.getType().equals(float.class))
	    		field.setFloat(o, (float) assignToDouble(value));
	    	else if(field.getType().equals(double.class))
	    		field.setDouble(o, (double) assignToDouble(value));
	    	else
	    		field.set(o, value);
    	}catch(Exception e){
    		throw new RuntimeException(e.getMessage(),e);
    	}
    }
	
	public static double callMethod(Method method, Object object, ArgumentList argumentList){
		return assignToDouble(callComplexMethod(method,object,argumentList));
	}
	
	public static boolean isAssignableToDouble(Class<?> clasz){
		if(clasz.equals(void.class))
			return false;
		
		if(isPrimitive(clasz)||isWrapper(clasz))
			return true;
		
		if (Shell.OP_PARSE_TRACE > 0) 
			System.out.println("not assignableToDouble: "+clasz);
		
		return false;
	}
	
	public static double assignToDouble(Object o){
		if(o instanceof Double){
			return (Double)o;
		}else if(o instanceof Float){
			return (Float)o;
		}else if(o instanceof Long){
			return (Long)o;
		}else if(o instanceof Short){
			return (Short)o;
		}else if(o instanceof Integer){
			return (Integer)o;
		}else if(o instanceof Character){
			return (Character)o;
		}else if(o instanceof Boolean){
			return ((Boolean)o)?1:0;
		}else if(o instanceof Byte){
			return (Byte)o;
		}else{
			return (double) (Double) o; //throw ClassCastException (generates perfect error description)
		}
	}
	
	public static Object convertFromDouble(Object o, Class<?> to){
		//straight (Integer -> Integer)
		if(o.getClass().equals(to))
			return o;
		
		Class<?> from = o.getClass();
		
		//straight primitives (Integer -> int)
		if(isWrapper(from) && to.getClass().equals(wrapperToPrimitive.get(from))) return wrapperToPrimitive.get(from).cast(o);
		
		//straight primitives (int -> Integer)
		if(isPrimitive(from) && to.getClass().equals(primitiveToWrapper.get(from))) return primitiveToWrapper.get(from).cast(o);
		
		//convert to double
		double d=1;
		if(o instanceof Integer){
			d=((Integer) o).intValue();
		}else if(o instanceof Long){
			d=((Long) o).longValue();
		}else if(o instanceof Double){
			d=((Double) o).doubleValue();
		}else if(o instanceof Float){
			d=((Float) o).floatValue();
		}else if(o instanceof Byte){
			d=((Byte) o).byteValue();
		}else if(o instanceof Short){
			d=((Short) o).shortValue();
		}else if(o instanceof Character){
			d=((Character) o).charValue();
		}else if(o instanceof Boolean){
			d=((Boolean) o).booleanValue()?1:0;
		}else{//not primitive -> route through
			return o;
		}
		
		Double dd=d;
		
		//convert from double
		if(to.equals(float.class)||to.equals(Float.class)){
			return dd.floatValue();
		}else if(to.equals(int.class)||to.equals(Integer.class)){
			return dd.intValue();
		}else if(to.equals(byte.class)||to.equals(Byte.class)){
			return dd.byteValue();
		}else if(to.equals(char.class)||to.equals(Character.class)){
			return (char)dd.intValue();
		}else if(to.equals(boolean.class)||to.equals(Boolean.class)){
			return !doubleEquals(dd.intValue(),0);
		}else if(to.equals(long.class)||to.equals(Long.class)){
			return dd.longValue();
		}else if(to.equals(short.class)||to.equals(Short.class)){
			return dd.shortValue();
		}else{
			return o;
		}
	}
	
	public static boolean isValidAssignment(Class<?> assignTo, Class<?> assignFrom){
		if(assignTo.isAssignableFrom(assignFrom)){
			if (Shell.OP_PARSE_TRACE > 0)
				System.out.println("valid: "+assignTo.getSimpleName()+" = "+assignFrom.getSimpleName());
			return true;
		}
		
		if(isNumeric(assignTo)&&isNumeric(assignFrom)){
			if (Shell.OP_PARSE_TRACE > 0)
				System.out.println("valid: "+assignTo.getSimpleName()+" = "+assignFrom.getSimpleName());
			return true;
		}
		
		return false;
	}
	
	public static Method searchMethod(Class<?> clasz, String name, ArgumentList argumentList, boolean staticAccess){
		Vector<Operation> args = argumentList.getArguments();
		
		if (Shell.OP_PARSE_TRACE > 0)
			System.out.println("Searching for method "+name+" with args "+args);
		
		for(Method mm : clasz.getMethods()){
			if(mm.getName().equalsIgnoreCase(name)){
				if (Shell.OP_PARSE_TRACE > 0)
					System.out.println("testing "+mm);
				
				if(staticAccess&&!Modifier.isStatic(mm.getModifiers())){
					if (Shell.OP_PARSE_TRACE > 0)
						System.out.println("  static access impossible");
					continue;
				}
					
				Class<?>[] params = mm.getParameterTypes();
				
				if(params.length==args.size() &&!mm.isVarArgs()){
					boolean allMatch = true;
					
					for(int i=0;i<params.length;i++){
						if(!isValidAssignment(params[i], args.get(i).getOutputType())){
							allMatch = false;
							break;
						}
					}
					
					if(allMatch){
						if (Shell.OP_PARSE_TRACE > 0)
							System.out.println("-> pass");
						return mm;
					}
				}else if(params.length<=args.size()+1&&mm.isVarArgs()){
					boolean allMatch = true;
					
					for(int i=0;i<params.length-1;i++){
						if(!isValidAssignment(params[i], args.get(i).getOutputType())){
							allMatch = false;
							break;
						}
					}
					
					if(allMatch){
						if (Shell.OP_PARSE_TRACE > 0)
							System.out.println("-> pass");
						return mm;
					}
				}else{
					if (Shell.OP_PARSE_TRACE > 0)
						System.out.println("   Invalid Param Count ("+args.size()+")");
				}
			}
		}
		
		if (Shell.OP_PARSE_TRACE > 0)
			System.out.println("-> fail");
		
		return null;
	}
	
	public static Constructor<?> searchConstructor(Class<?> clasz, Vector<Operation> args){
		if (Shell.OP_PARSE_TRACE > 0)
			System.out.println("Searching for constructor of "+clasz+" with args "+args);
		
		for(Constructor<?> c : clasz.getConstructors()){
				
			Class<?>[] params = c.getParameterTypes();
			
			if(params.length==args.size() &&!c.isVarArgs()){
				boolean allMatch = true;
				
				for(int i=0;i<params.length;i++){
					if(!isValidAssignment(params[i], args.get(i).getOutputType())){
						allMatch = false;
						break;
					}
				}
				
				if(allMatch){
					if (Shell.OP_PARSE_TRACE > 0)
						System.out.println("-> pass");
					return c;
				}
			}else if(params.length<=args.size()+1&&c.isVarArgs()){
				boolean allMatch = true;
				
				for(int i=0;i<params.length-1;i++){
					if(!isValidAssignment(params[i], args.get(i).getOutputType())){
						allMatch = false;
						break;
					}
				}
				
				if(allMatch){
					if (Shell.OP_PARSE_TRACE > 0)
						System.out.println("-> pass");
					return c;
				}
			}else{
				if (Shell.OP_PARSE_TRACE > 0)
					System.out.println("   Invalid Param Count ("+args.size()+")");
			}
		}
		
		if (Shell.OP_PARSE_TRACE > 0)
			System.out.println("-> fail");
		
		return null;
	}
	
	public static boolean doubleEquals(double d1, double d2){
		return Math.abs(d1-d2)<1e-10 || Math.abs(d2-d1)/d2 < 1e-10; //absolute or relative error must be smaller than 1e-10 (relative error near zero causes problems otherwise)
	}
}
